/**
 * Actively Replicated Crash Free Single Software Failure (non Byzantine)
 * Distributed Retail Store 
 * 
 * November 28 2012
 */
package comp6231.project.common;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;

/**
 * Implementation of reliable UDP using NACK protocol
 * @author dimitri.tiago
 */
public class ReliableUDPSocket
{
	private DatagramSocket udpSocket;
	private DatagramPacket sendPacket;
	private DatagramPacket receivePacket;
	private InetAddress serverAddress;
	private int serverPort;
	
	private byte [] sentData;
	
	UDPTimer udpTimer;
	
	/**
	 * Default constructor initializes udp datagram socket and timeout timer.
	 */
	public ReliableUDPSocket()
	{
		try 
		{
			udpSocket = new DatagramSocket();	
		} 
		catch (SocketException e) 
		{
			e.printStackTrace();
			udpSocket.close();
		}
	}
	
	/**
	 * Constructor initializes udp datagram socket and timeout timer
	 * and binds the socket to the specified port.
	 * @param port port to bind socket
	 */
	public ReliableUDPSocket(int port)
	{
		try 
		{
			udpSocket = new DatagramSocket(port);	
		} 
		catch (SocketException e) 
		{
			e.printStackTrace();
			udpSocket.close();
		}
	}
		
	/**
	 * Send datagram packet using udp reliable 1-1 communication.
	 * @param p datagram packet to send.
	 */
	public void send(DatagramPacket p)
	{
		// prepare and send request
		UDPMessage requestMessage = new UDPMessage();
		requestMessage.setHeader( "DATA" );
		requestMessage.setBody( p.getData() );
					
		sendPacket = p;
		byte[] bRequestMessage = Utils.toBytes(requestMessage);
		sendPacket.setData(bRequestMessage);
		
		resend(sendPacket, true);
		
		// wait for ack indicating request received at
		// destination
		try 
		{
			udpSocket.receive(p);
			
			UDPMessage message = 
					(UDPMessage) Utils.fromBytes( p.getData() );
			String messageHeader = message.getHeader();
			if ( messageHeader.equalsIgnoreCase("ACK") )
			{
				// message received at server.
				udpTimer.cancel(); // cancel timer timeout
				sentData = null;   // reset re-send buffer
			}
			else
			{
				// incorrect message received. send again.
				UDPMessage sentMessage = (UDPMessage) Utils.fromBytes( sendPacket.getData() );
				byte [] sentData = sentMessage.getBody();
				DatagramPacket resendPacket = new DatagramPacket(sentData, sentData.length, sendPacket.getAddress(), sendPacket.getPort());
				send(resendPacket);
			}	
		} 
		catch (IOException e) 
		{
			e.printStackTrace();
		}		
	}
	
	/**
	 * Resend message and set timeout timer to start/not start.
	 * @param bMessage udp message to send.
	 * @param startTimer start/do not start timeout timer.
	 */
	private void resend(DatagramPacket sendPacket, Boolean startTimer)
	{
		try 
		{
			udpSocket.send(sendPacket);
			
			// start re-send timer
			if (startTimer == true)
			{
				udpTimer = new UDPTimer(this, 300);
				udpTimer.start();
				
			}
		} 
		catch (IOException e) 
		{
			e.printStackTrace();
			udpSocket.close();
		}
	}
	
	//TODO: does receive have to loop on true to guarantee ack reaches server?
	/**
	 * Receive udp datagram packet using reliable udp 1-1 communication.
	 * @param p datagram packet to receive.
	 */
	public void receive(DatagramPacket p)
	{
		try 
		{
			udpSocket.receive(p);
			
			UDPMessage message   = (UDPMessage) Utils.fromBytes( p.getData() );
			String messageHeader = message.getHeader();
			if ( messageHeader.equalsIgnoreCase("DATA") )
			{
				// send ack for response message back to server
				UDPMessage ack = new UDPMessage();
				ack.setHeader("ACK");
				sendPacket = p;
				sendPacket.setData(Utils.toBytes(ack));
				resend(sendPacket, false);
				
				// set return data
				sendPacket.setData(message.getBody()); 
				//TODO: close socket?
			}
		} 
		catch (IOException e) 
		{
			e.printStackTrace();
			udpSocket.close();
		}
	}
			
	/**
	 * Re-send sent message on timer timeout
	 */
	public void timerUpdate()
	{
		resend(sendPacket, true);
	}
	
	/**
	 * Close UDP Socket
	 */
	public void closeSocket()
	{
		udpSocket.close();
	}
}